package sa.Master.Server.Modules;

import sa.Master.common.NetworkMessages.Validated.*;
import sa.Master.common.Classes.*;
import sa.Master.common.Functions.*;
import sa.Master.Server.Server.Field;

import java.io.*;

import com.google.gson.JsonParseException;

/**
 * Process a received Request and generate a response
 * 
 * This class just do some local work, not Internet-related.
 * It parse the request and communicate with other modules to complete works like:
 *      permission check
 *      operating database
 * Then pack the information from other modules into response.
 * 
 * @author whsu
 * @see sa.Master.common.NetworkMessages.Validated.Request for class Request's structure and standards.
 * @see sa.Master.common.NetworkMessages.Validated.Response for class Response's structure and standards.
 */
public class RequestHandler {
    
    private final UserInfoManager userInfoManager;
    private final PropertyManager propertyManager;
    private final ImageManager imageManager;
    private final String userName;

    /**
     * @param userInfoManager inherited from Server 
     * @param propertyManager inherited from Server
     * @param imageManager inherited from Server
     * @param userName After a successful login, the login user's name of the session.
     */
    public RequestHandler(UserInfoManager userInfoManager, PropertyManager propertyManager, ImageManager imageManager, String userName) {

        this.userInfoManager = userInfoManager;
        this.propertyManager = propertyManager;
        this.imageManager = imageManager;
        this.userName = userName;

    }

    /**
     * Parse and distribute request and Generate response.
     * 
     * Read the actions of the requests, and distribute it to correspond handling method.
     * Generate response according to the information returned by the methods.
     * 
     * @param request Received request.
     * @return The response ready for sending to client.
     * @throws LogoutException on
     *      Received logout request from client.
     */
    public Response getResponse(Request request) throws LogoutException {

        Response response = null;

        try {
            if (!this.permissionCheck(request)) {
                throw new RequestHandleException("request handle: Permission denied.");
            } // else

            switch (request.action) {
                case "show":
                    response = new Response.Builder(true)
                            .addReturnData("text", this.show(request))
                            .addReturnData("image", this.showImage(request))
                            .build();
                    break;
                case "create":
                    this.create(request);
                    this.updateImage(request);
                    response = new Response.Builder(true)
                            .addReturnData("text", null)
                            .build();
                    break;
                case "update":
                    this.update(request);
                    this.updateImage(request);
                    response = new Response.Builder(true)
                            .addReturnData("text", null)
                            .build();
                    break;
                case "delete":
                    this.delete(request);
                    response = new Response.Builder(true)
                            .addReturnData("text", null)
                            .build();
                    break;
                case "logout":
                    throw new LogoutException();
                default:
                    throw new RequestHandleException(String.format("request handle: Action %s: Not supported.", request.action));
            }
        } catch (LogoutException e) {
            throw e;
        } catch (Exception e) {
            response = new Response.Builder(false)
                                              .addReturnData("text", e.getMessage())
                                              .build();
        }
        return response;

    }






    /**
     * Return PropertyManager's field description(personal, group) according to the request's description.
     * 
     * @param request Received request
     * @return PropertyManaget's field description
     * @see PropertyManager
     * @see Request#description
     */
    private Field getField(Request request) {
        if (request.description.get("personal")!=null) {
            return Field.personal;
        } else if (request.description.get("group")!=null) {
            return Field.group;
        } else {
            throw new RequestHandleException("request handle: field description \"personal\" and \"group\": Not exist.");
        }
    }

    /**
     * Return the subject name(user's name or group's name) according to the request's description.
     * 
     * @param request Received request
     * @return User or group's name according to the request's description.
     * @see sa.Master.common.NetworkMessages.Validated.Request#description
     */
    private String getSubjectName(Request request) {
        String subjectString = null;
        if ((subjectString = (String) request.description.get("personal"))!=null) {
            return this.userName;
        } else if ((subjectString = (String) request.description.get("group"))!=null) {
            return subjectString;
        } else {
            throw new RequestHandleException("request handle: field description \"personal\" and \"group\": Not exist.");
        }
    }

    /**
     * Return the table's name according to the request's description.
     * 
     * @param request Received request
     * @return Table's name according to the request's description.
     * @see sa.Master.common.NetworkMessages.Validated.Request#description
     */
    private String getTableName(Request request) throws RequestHandleException{
        String tableName = (String) request.description.get("table");
        if (tableName!=null) {
            return tableName;
        } else {
            throw new RequestHandleException("request handle: table name description \"table\": Not exist.");
        }
    }

    /**
     * Return the item's name according to the request's description.
     * 
     * @param request Received request
     * @return Item's name according to the request's description.
     * @see sa.Master.common.NetworkMessages.Validated.Request#description
     */
    private String getItemName(Request request) {
        String itemName = (String) request.description.get("item");
        if (itemName!=null) {
            return itemName;
        } else {
            throw new RequestHandleException("request handle: item name description \"item\": Not exist.");
        }
    }

    /**
     * Return the item's updated or initial(create) json data according to the request's description.
     * 
     * @param request Received request
     * @return Item's json data according to the request's description.
     * @see sa.Master.common.NetworkMessages.Validated.Request#description
     */
    private String getItemJson(Request request) {
        return (String) request.description.get("data");
    }

    /**
     * Check if the request is violating the permission.
     *
     * If the request requires a personal field information, then it's valid.
     * If the request requires a group field information:
     *      The {@code create} and {@code delete} action are only for administrators.
     *
     * @param request Received request.
     * @return true if valid.
     */
    private boolean permissionCheck(Request request) {

        // Always welcome logout
        if ("logout".equals(request.action)) {
            return true;
        }

        String subjectNameString = null;

        // Personal operation is always valid.
        if (request.description.get("personal")!=null) {
            return true;
        }
        // Group operation:
        // Delete and Create requires admin permission
        // Other actions requires member permission.
        else if ((subjectNameString = (String) request.description.get("group"))!=null) {
            if ("create".equals(request.object) || "delete".equals(request.object)) {
                return this.userInfoManager.validateGroup(this.userName, subjectNameString) &&
                       this.userInfoManager.validateAdmin(this.userName, subjectNameString);
            } else {
                return this.userInfoManager.validateGroup(this.userName, subjectNameString);
            }
        } else {
            throw new RequestHandleException("request handle: field description \"personal\" and \"group\": Not exist.");
        }
    }






    /**
     * The method handling show action.
     *
     * @param request Received request.
     * @return The result of the action in Json format.
     * @throws FileNotFoundException on
     *      1. Required tables directory(directory storing all the tables) for the subject(user, group) not found.
     *      2. Required table belonging to the subject not found.
     *      3. Required item in the specified table not found.
     * @throws IOException on
     *      1. (When required to show the item) Failed to read the item file.
     * @throws RequestHandleException
     *      1. The object specified is not supported. (Illegal object for action "show")
     * @see sa.Master.Server.Modules.PropertyManager public methods begin with "show" for more detailed Exception statement.
     */
    private String show(Request request) throws FileNotFoundException, IOException, RequestHandleException {

        if ("tables".equals(request.object)) {
            return this.propertyManager.showTables(
                this.getField(request),
                this.getSubjectName(request)
            );
        } else if ("items".equals(request.object)) {
            return this.propertyManager.showItems(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request)
            );
        } else if ("item".equals(request.object)) {
            return this.propertyManager.showItem(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request),
                this.getItemName(request)
            );
        }



        // Returning the json of userInfo.
        else if ("userInfo".equals(request.object)){
            return userInfoManager.getUserInfo(this.userName);
        }


        // unsupported
        else {
            throw new RequestHandleException(String.format("request handle: Action show: object %s: not supported.", request.object));
        }

    }

    /**
     * The method handling create action.
     *
     * @param request Received request.
     * @throws IOException on
     *      1. Table creating failed.
     *      2. Item creating failed due to file writing error.
     * @throws JsonParseException on
     *      1. Item creating failed due to item json format error.
     * @throws RequestHandleException
     *      1. The object specified is not supported. (Illegal object for action "create")
     * @throws DuplicateException on
     *      1. There's an object of the same type with the same name already exists.
     * @see sa.Master.Server.Modules.PropertyManager public methods begin with "create" for more detailed Exception statement.
     * @see {@code docs/jsons.md} for json standards.
     */
    // TODO docs
    private void create(Request request) throws IOException, JsonParseException, RequestHandleException, DuplicateException {

        if ("table".equals(request.object)) {
            this.propertyManager.createTable(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request)
            );
        } else if ("item".equals(request.object)) {
            this.propertyManager.createItem(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request),
                this.getItemName(request),
                this.getItemJson(request)
            );
        } else if ("group".equals(request.object)) {
            String newGroupName = (String) request.description.get("group");
            this.propertyManager.createGroup(newGroupName);
            this.userInfoManager.createGroup(this.userName, newGroupName);
        } else {
            throw new RequestHandleException(String.format("request handle: Action create: object %s: not supported.", request.object));
        }

    }

    /**
     * The method handling update action.
     *
     * @param request Received request.
     * @throws IOException on
     *      1. Item updating failed due to file writing error.
     * @throws JsonParseException
     *      1. Item updating failed due to item json format error.
     * @throws LockException on
     *      1. A valid lock for that object already exist.
     * @throws RequestHandleException
     *      1. The object specified is not supported. (Illegal object for action "update")
     * @see sa.Master.Server.Modules.PropertyManager public methods begin with "update" for more detailed Exception statement.
     * @see {@code docs/jsons.md} for json standards.
     */
    private void update(Request request) throws IOException, JsonParseException, LockException, RequestHandleException {

        // if Json is empty, then skip updating.
        String itemJson = this.getItemJson(request);

        if ("item".equals(request.object) && !("".equals(itemJson)) && itemJson!=null) {
            this.propertyManager.updateItem(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request),
                this.getItemName(request),
                itemJson
            );
        } else if ("password".equals(request.object)) {
            this.userInfoManager.updatePassword(
                    this.userName,
                    (String) request.description.get("newPassword"),
                    (String) request.description.get("oldPassword")
            );
        } else {
            throw new RequestHandleException(String.format("request handle: Action update: object %s: not supported.", request.object));
        }

    }

    /**
     * The method handling delete action.
     *
     * @param request
     * @throws IOException on
     *      1. Delete not successful (After deletion, the object still exists).
     * @throws LockException on
     *      1. A valid lock for that object already exist.
     * @see sa.Master.Server.Modules.PropertyManager public methods begin with "delete" for more detailed Exception statement.
     */
    private void delete(Request request) throws IOException, LockException {

        if ("table".equals(request.object)) {
            this.propertyManager.deleteTable(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request)
            );
        } else if ("item".equals(request.object)) {
            this.propertyManager.deleteItem(
                this.getField(request),
                this.getSubjectName(request),
                this.getTableName(request),
                this.getItemName(request)
            );
        } else {
            throw new RequestHandleException(String.format("request handle: Action delete: object %s: not supported.", request.object));
        }

    }






    /**
     * Show a specified image
     *
     * Return type is a byte array converted from BufferedImage by sa.Master.common.Functions.ImageUtilities
     *
     * @param request Received request.
     */
    private byte[] showImage(Request request) {

        byte[] image = null;
        String imageObjectName = null;

        try {
            if ("item".equals(request.object)) {
                imageObjectName = "image";
                image = this.imageManager.showItemImage(
                        Field.personal,
                        this.getSubjectName(request),
                        this.getTableName(request),
                        this.getItemName(request)
                                                       );
            } else if ("avatar".equals(request.object)) {
                imageObjectName = "avatar";
                image = this.imageManager.showAvatar(userName);
            }
        } catch (IOException e) {
            LoggingUtilities.syserr(String.format("show %s: warning: %s", imageObjectName, e.getMessage()));
        }

        return image;

    }

    /**
     * Update or Create a specified image.
     *
     * @param request Received request.
     */
    private void updateImage(Request request) {

        byte[] image = (byte[]) request.description.get("image");
        String imageObjectName = null;

        if (image==null) {
            LoggingUtilities.syserr("update image/avatar: warning: Image not specified.");
            return;
        }

        try {
            if ("item".equals(request.object)) {
                imageObjectName = "image";
                this.imageManager.updateItemImage(
                        Field.personal,
                        this.getSubjectName(request),
                        this.getTableName(request),
                        this.getItemName(request),
                        image
                                                 );
            } else if ("avatar".equals(request.object)) {
                imageObjectName = "avatar";
                this.imageManager.updateAvatar(userName, image);
            }
        } catch (IOException e) {
            LoggingUtilities.syserr(String.format("update %s: warning: %s", imageObjectName, e.getMessage()));
        }

    }
}
